iT邦幫忙

2026 iThome 鐵人賽

DAY 17
2

前幾篇,我們已經從「事情的先後順序」一路談到有向圖、Cycle、DAG 與拓樸排序。
例如:

A → B → C

代表:

B 依賴 AC 又依賴 B

這種 dependency 並不只存在於修課、專案流程或 Build Pipeline。
如果你每天都在寫 JavaScript,其實你早就在跟 Dependency Graph 打交道了。
只是它平常長得比較像這樣:

package.json
node_modules/

今天我們就把 Graph 拉回一個非常熟悉的主戰場:

npm 專案


安裝一個 package,真的只是在多一個資料夾嗎?

假設我們有一個很簡單的前端專案:

app
├ React
├ Router
└ Library A
      └ Library B

光從資料夾或套件名稱看,很容易把它想成:

app
├─ React
├─ Router
└─ Library A
   └─ Library B

這看起來非常像一棵 Tree。
但如果我們不要看「資料夾放在哪裡」,而是問:

誰依賴誰?

整件事情就會變成另一個模型。
不過在畫圖之前,要先統一箭頭的方向。
這個系列延續前幾篇的規則:

被依賴的套件 → 使用它的套件

因此 A → B 代表:

B 依賴 A,必須先有 AB 才能使用它

有些資料會把箭頭反過來,從「依賴者」指向「被依賴者」。
兩種畫法都有人使用,所以閱讀 Dependency Graph 時,最重要的是先確認作者採用哪一種方向。
假設 app 直接使用 ReactRouterLibrary A,而 Library A 又依賴 Library B
按照剛才的方向,Graph 會是:
https://ithelp.ithome.com.tw/upload/images/20260904/20129020yDa9n8qRfP.png

這時每個 package 都可以看成一個 node。
圖中的箭頭都從被依賴的套件指向使用它的套件。
換句話說,npm 專案本身就是一張:

Dependency Graph


Direct Dependency:你直接說「我要它」

先看最簡單的一種 dependency。
假設你的 package.json 是:

{
  "dependencies": {
    "react": "...",
    "some-library": "..."
  }
}

那麼對你的 app 來說:

react → app
some-library → app

這些就是:

直接依賴 Direct Dependency

也就是你直接宣告:

我的專案需要這些 package

Graph 可以畫成:
https://ithelp.ithome.com.tw/upload/images/20260907/20129020QwfdpgAlMd.png

這一層的 dependency 通常最好理解,因為它們就在你的 package.json 裡。


但你的 dependency 往往不只這些

更常遇到的問題是 some-library,本身也可能依賴別的 package。
例如:

Library B → Library A → app

雖然你的 package.json 裡可能根本沒有寫 Library B,但你的專案最後還是需要它。
因為只要 Library A 需要 Library B,那麼 app 間接也會受到 Library B 的影響。
這種關係通常稱為:

間接依賴 Transitive Dependency


Direct 與 Transitive 的差別

可以把它們放在一起看:

Library B → Library A → app

app 來說,Library A直接依賴 direct dependency
Library B 則是間接依賴 transitive dependency

你沒有直接指定 Library B,但它仍然存在於你的 dependency graph 裡。
這也是為什麼一個看起來只有十幾個 dependencies 的專案,最後可能安裝出數百甚至上千個 packages。
你以為只是安裝了幾個 package,但實際上是:

我選擇的 package,又分別帶進了多少 dependency


Dependency 會繼續延伸

實際的情況可能是:
https://ithelp.ithome.com.tw/upload/images/20260907/20129020siU0Dz9ViI.png

BCE 又可能各自依賴其他 package:
https://ithelp.ithome.com.tw/upload/images/20260907/20129020UOALjlNlNp.png

如果再繼續展開,就會形成一整張 dependency graph,所以 package manager 真正在處理的,不只是一份 package 清單,而是:

一整組彼此連接的 dependency relationship


Shared Dependency:兩個 package 可能需要同一個東西

假設:

Utility → Library A
Utility → Library B

同時 app 又依賴 AB

Library A → app
Library B → app

整張圖會變成:
https://ithelp.ithome.com.tw/upload/images/20260907/20129020ZrAoPh3WD5.png

這裡的 Utility 同時被兩個 package 使用。
這就是:

Shared Dependency

也就是同一個 dependency 同時連向多個使用它的 node。

注意這個結構已經不是 Tree 了。
因為同一個 Utility 同時參與了兩條 dependency relationship:

Utility → Library A
Utility → Library B

如果硬畫成套件階層,就必須重複放置 Utility,或假裝它只屬於其中一個 package。
Graph 則可以直接保留這種共用關係。


這也是 Tree 與 Graph 很重要的差別

假設你只看 node_modules

node_modules/
├─ library-a/
├─ library-b/
└─ utility/

它就是一個資料夾階層。
從檔案系統的角度來看:

它當然是一棵 Tree

因為資料夾本來就有 parent / child 的階層關係。

但這不代表:

package 之間的 dependency relationship 也是 Tree

這兩件事情描述的是完全不同的問題。


「放在哪裡」和「依賴誰」不是同一種關係

這裡非常需要分開看,檔案系統描述的是:

這個檔案放在哪個資料夾下面?

例如:

node_modules
└─ utility

這是一種:

包含關係 containment relationship

但 package dependency 描述的是:

誰需要誰?

例如:

Utility → Library A
Utility → Library B

這是一種:

依賴關係 dependency relationship

所以同一批資料,可以同時存在兩種完全不同的結構:

  • File System → Tree
  • Package Dependency → Graph

這也再次呼應我們前面一直在談的一件事情:

資料長得一樣,不代表我們應該用同一種方式描述它

要去思考的是:

你現在想表達的是哪一種關係?


為什麼 package dependency 很難是一棵 Tree?

因為真實的軟體套件很常共用基礎功能。
例如:

  • React → Router
  • React → UI Library
  • React → State Library

如果你的 app 同時使用:

  • Router
  • UI Library
  • State Library

那可能形成:
https://ithelp.ithome.com.tw/upload/images/20260907/20129020aR82QdesCr.png

更準確一點可以寫成:

React → Router
React → UI Library
React → State Library

Router → app
UI Library → app
State Library → app

React 不是某一個 package 專屬的 child,它是多個 package 都可能共用的前置 dependency。
所以:

package dependency 更接近 Graph,而不是 Tree


同一個 dependency,版本還可能不同

事情還可以更複雜,假設:

Utility v1 → Library A
Utility v2 → Library B

那麼表面上它們都叫 Utility,但實際 dependency constraint 並不相同。
例如:

Utility v1
    ↓
Library A

Utility v2
    ↓
Library B

package manager 就必須判斷:

這兩個需求能不能共用同一個版本?

如果可以,可能共用。
如果不能,就可能需要同時存在不同版本。
這時真正的 dependency graph 已經不是單純 package name → package name,還包含了 packageversionversion constraint 等資訊。
不過今天先不深入 npm 的完整 dependency 解決方案。
我們只需要先建立一個重要直覺:

package manager 面對的是 Graph,不只是資料夾


Cycle 在 npm 世界裡也可能出現

前面 Day 14 我們談過 A → B → C → A 這叫循環 Cycle
package dependency 當然也可能發生類似的事情。
例如:

Package A → Package B
Package B → Package A

畫成:

A → B
↑   ↓
└───┘

這就是 circular dependency。
在 JavaScript 開發裡,你可能也看過類似的 module dependency:

// a.js
import { b } from "./b.js";
// b.js
import { a } from "./a.js";

形成:

a.js → b.js
 ↑       ↓
 └───────┘

這裡要注意:

package dependency 與 JavaScript module dependency 是不同層級的 Graph

一個是 package → package,另一個是 module → module,但它們背後面對的結構問題其實非常類似:

dependency 會不會形成循環?


那 package manager 是不是直接照拓樸排序安裝?

上一篇我們談到:

如果 dependency graph 是 DAG,就可以透過拓樸排序找到符合依賴約束的順序

所以很容易想到 B → A → app 是不是應該:

先裝 B
再裝 A
最後處理 app

這個直覺是正確的,dependency graph 確實可以幫助我們理解:

哪些東西依賴哪些東西

但真實的 npm、pnpm、Yarn 所做的事情比單純拓樸排序複雜得多。
它們還要處理:

version resolution
deduplication
peer dependency
optional dependency
lockfile
package layout
...

等等。

所以這裡不要把:

npm install = Topological Sort

直接畫上等號。
比較精確的說法是:

Dependency Graph 提供了 package manager 必須處理的核心結構

Topological ordering 只是其中一種可以從 Graph 推導出的資訊。


今天真正要記住的不是 node_modules

node_modules 只是今天拿來建立直覺的入口。
真正重要的是這件事:

package.json
      ↓
Dependency Graph

當你看到 A depends on B 時,它不只代表:

安裝 A 時還需要 B

它其實是在建立一條 B → A 的 edge,當整個系統裡存在大量 dependency 時,我們面對的就不再是一份清單,而是一張 Graph,而且這張 Graph 可能包含:

direct dependency
transitive dependency
shared dependency
cycle

也因此:

node_modules 看起來像資料夾 Tree,不代表 package relationship 本身就是 Tree

因為 Tree 描述的是階層,而 Dependency Graph 描述的是依賴。


留給下一篇的問題

到目前為止,我們主要用 Dependency Graph 回答:

誰依賴誰,以及誰應該先處理?

但如果系統已經建立好了,某個上游資料突然改變。
這時真正需要解決的問題就變成:

哪些下游結果會受到影響,又必須重新處理?

下一篇,我們就從這個問題開始。


上一篇
Day 15|如果大家都有先後關係,要怎麼排順序?
系列文
生活中的資料結構與演算法:30 天學會把現實問題變成可推理的模型17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言